-- tdc.lua:  Traitor debugging utility
--
-- Implements the "/td" chat command.
-- Type "/td help" for info on how to use it.

local TAB = '   '

local function _keyw(msg)
    return minetest.colorize('#a0a0ff', msg)
end

local function _info(msg)
    return minetest.colorize('#ffff80', msg)
end

local function _lsitem(item)
    return TAB .. '- ' .. item
end

function list_actions()
    local hdr = 'Available commands: '
    local msgtab = {}
    for cmd in pairs(tdc.actions) do
        table.insert(msgtab, _keyw(cmd))
    end
    return hdr .. table.concat(msgtab, '|')
end

-- return corpse count / info messages for a list <ctab> of node positions
local function list_corpses(ctab)
    local node, meta
    local count = 0
    local corpses = {}
    for _, pos in ipairs(ctab) do
        node = minetest.get_node(pos)
        meta = minetest.get_meta(pos)
        if node and node.name and node.name ~= 'air' then
            local cinfo = meta:get_string('infotext')
            local cname = cinfo:match("[^']+") or node.name
            table.insert(corpses, _lsitem(cname .. ' ' .. minetest.pos_to_string(pos)))
        else
            table.insert(corpses, _lsitem('<nil> ' .. minetest.pos_to_string(pos)))
        end
        count = count + 1
    end
    return count, corpses
end


tdc = {
    fix_corpses = function(plr_name, map)
        local pmap = map or '*'
        local count = 0

        for mapid, cplist in pairs(lobby.corpses) do
            if pmap == '*' then
                lobby.corpse_removal(mapid)
                lobby.corpses[mapid] = {}
                count = count + 1
            else
                if mapid:find(pmap) then
                    lobby.corpse_removal(mapid)
                    lobby.corpses[mapid] = {}
                    count = count + 1
                    minetest.chat_send_player(plr_name,
                        _info('Corpses in ' .. _keyw(mapid) .. ' fixed.'))
                    break
                end
            end
        end
        if count == 0 then
            minetest.chat_send_player(plr_name, _info('No maps found'))
        elseif pmap == '*' then
            minetest.chat_send_player(plr_name,
                _info('Corpses in ' .. tostring(count) .. ' map(s) fixed.'))
        end
    end,

    fix_map = function(plr_name, map)
        local pmap = map or '*'
        local count = 0

        for mapid, count in pairs(lobby.map) do
            if pmap == '*' then
                lobby.map[mapid] = 0
                lobby.update_maps(mapid)
                count = count + 1
            else
                if mapid:find(pmap) then
                    lobby.map[mapid] = 0
                    lobby.update_maps(mapid)
                    count = count + 1
                    minetest.chat_send_player(plr_name,
                        _info('Map status of ' .. _keyw(mapid) .. ' fixed.'))
                    break
                end
            end
        end
        if count == 0 then
            minetest.chat_send_player(plr_name, _info('No maps found'))
        elseif pmap == '*' then
            minetest.chat_send_player(plr_name,
                _info('Status data for ' .. tostring(count) .. ' map(s) fixed.'))
        end
    end,
}

-- tdc.actions: enumerates the possible /td commands
-- tdc.actions = { cmd_1 = def_1, cmd_2 = def_2, ... }
--
-- command definitions are maps:
-- def_n = {
--      info = (string, short description for "/td help")
--      help = (0-arg function, returns a longer description for "/td help <action>")
--      exec = (2-arg function, executes the action for player named <arg1> with the rest
--              of the cmdline string passed in as <arg2>)
-- }
tdc.actions = {
    -- CMD: /td help
    help = {
        info = 'Show general help and list available commands',
        help = function()
            local msgtab = {
                'Traitor debugging utility.  Type "' .. _keyw('/td <cmd>') .. '" to execute a command.',
                'Available commands:'
            }
            for cmd, action in pairs(tdc.actions) do
                table.insert(msgtab, string.format(TAB .. '%-10s\t%s', cmd, action.info))
            end
            table.insert(msgtab, '\nCommands can be abbreviated (e.g., "/td h" shows this help).')
            table.insert(msgtab, 'Type "' .. _keyw('/td help <cmd>') .. '" to get help for a specific command.')
            return table.concat(msgtab, '\n')
        end,
        exec = function(plr_name, params)
            if params then
                local par1 = params:match('[%w_]+')
                if par1 and tdc.actions[par1] then
                    minetest.chat_send_player(plr_name, tdc.actions[par1].help())
                elseif par1 then
                    -- TODO: find par1 as cmd prefix in tdc.actions
                    local msg = _info('Unknown command "' .. par1 .. '"\n') .. list_actions()
                    minetest.chat_send_player(plr_name, msg)
                else
                    minetest.chat_send_player(plr_name, tdc.actions['help'].help())
                end
            else
                minetest.chat_send_player(plr_name, tdc.actions['help'].help())
            end
        end,
    },
    -- CMD: /td corpses
    corpses = {
        info = 'Show info on corpses',
        help = function()
            local msgtab = {
                _info('Usage: /td corpses [<map>]'),
                'Show corpses in map <map>.  If <map> is omitted, list all corpses.',
                'Params:',
                TAB .. '<map>   map id to search, or "*" to list all corpses'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(plr_name, params)
            local map = params:match('[%w_]+') or '*'
            local hdr, msg, ccount, clist
            if params == '' then
                -- list all corpses instead
                hdr = _info('List of corpses:\n')
                for mid, ctab in pairs(lobby.corpses) do
                    local corpses
                    if ctab then ccount = list_corpses(ctab) end
                    if ccount > 0 then
                        msg = (msg or '') .. _lsitem(mid .. ': ' .. tostring(ccount)) .. '\n'
                    end
                end
            else
                for mid, ctab in pairs(lobby.corpses) do
                    if ctab and (mid:find(map) or map == '*') then
                        hdr = _info('Corpses in ') .. _keyw(mid) .. ':\n'
                        ccount, clist = list_corpses(ctab)
                        if ccount > 0 then
                            msg = (msg or '') .. hdr .. table.concat(clist, '\n') .. '\n'
                        end
                        hdr = ''
                        if map ~= '*' then break end
                    end
                end
            end
            if msg then
                msg = hdr .. msg
            else
                msg = _info('No corpses yet.')
            end
            minetest.chat_send_player(plr_name, msg)
            minetest.log('action', minetest.strip_colors(msg))
        end,
    },
    -- CMD: /td maps
    maps = {
        info = 'Show maps currently visited by players',
        help = function()
            local msgtab = {
                _info('Usage: /td maps'),
                'Show map names of all currently active game sessions.'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(plr_name, params)
            local msgtab = {_info('Active maps:')}
            local plr_count = 0
            local msg
            for mapid, plr_count in pairs(lobby.map) do
                local gh_count = 0
                local gh_map = mapid .. '_ghost'
                for _, mid in pairs(lobby.game) do
                    if mid == gh_map then
                        gh_count = gh_count + 1
                    end
                end
                if plr_count > 0 or gh_count > 0 then
                    table.insert(msgtab,
                        _lsitem(mapid .. ': ' .. plr_count .. ' player(s)')
                            .. ' / ' .. _keyw(tostring(gh_count) .. ' ghost(s)'))
                end
            end
            local clobby = 0
            for _, mid in pairs(lobby.game) do
                if mid == 'lobby' then
                    clobby = clobby + 1
                end
            end
            if clobby > 0 then
                table.insert(msgtab, _lsitem('lobby: ' .. tostring(clobby) .. ' player(s)'))
            end
            msg = table.concat(msgtab, '\n')
            minetest.chat_send_player(plr_name, msg)
            minetest.log('action', minetest.strip_colors(msg))
        end,
    },
    -- CMD: /td traitors
    traitors = {
        info = 'Show the traitors in each active map',
        help = function()
            local msgtab = {
                _info('Usage: /td traitors'),
                'Show the names of all traitors in currently active game sessions'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(plr_name, params)
            local msg = ''
            for mapid, traitor in pairs(lobby.traitors) do
                -- table value could be nil if entry is to be GC'd
                if traitor then
                    msg = msg .. _lsitem(mapid .. ': ' .. traitor) .. '\n'
                end
            end
            if msg == '' then
                msg = _info('No active traitors!')
            else
                msg = _info('Active traitors:\n') .. msg
            end
            minetest.chat_send_player(plr_name, msg)
            minetest.log('action', minetest.strip_colors(msg))
        end,
    },
    -- CMD: /td player <id>
    player = {
        info = 'Show player attributes',
        help = function()
            local msgtab = {
                _info('Usage: /td player <id>'),
                'List some player metadata.',
                'Params:',
                TAB .. '<id>    player name (substring suffices).',
                '\nNote: <id> is restricted to alphanumeric chars and "_", for the sake of security.'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(plr_name, params)
            if not params or not params:find("[%w_]+") then
                minetest.chat_send_player(plr_name, 'Missing argument, type "/td help player" for help.')
            else
                local p1, p2, plid = params:find('([%w_]+)')
                local count = 0

                for _, player in pairs(minetest.get_connected_players()) do
                    local pname = player:get_player_name()

                    if pname:find(plid) then
                        count = count + 1
                        local attr = player:get_meta()
                        local mtab = {
                            _info('Attributes of ') .. _keyw(pname) .. ':',
                            TAB .. 'ghost:  ' .. (attr:get_string('ghost') or 'false'),
                            TAB .. 'spawn:  ' .. (attr:get_string('spawn_pos') or '<nil>'),
                            TAB .. 'voting: ' .. (attr:get_string('voting') or 'false')
                        }
                        minetest.chat_send_player(plr_name, table.concat(mtab, '\n'))
                    end
                end
                if count == 0 then
                    minetest.chat_send_player(plr_name, _info('No matching player'))
                end
            end
        end,
    },
    -- CMD: /td fix (corpses|map) <map>
    --[[
        fix corpses:    call lobby.corpse_removal(<map>), then reset its poslist 
        fix maps:       set lobby.map[<map>] = 0, then call lobby.update_maps(<map>)
    --]]
    fix = {
        info = 'Fix internal data',
        help = function()
            local msgtab = {
                _info('Usage: /td fix <type> <map>'),
                'Try to repair damages to game data.',
                'Params:',
                TAB .. '<type>  One of the following:',
                TAB .. '        ' .. _keyw('corpses') .. ' -- fix lobby.corpses',
                TAB .. '        ' .. _keyw('maps') .. '    -- fix lobby.map and player status',
                TAB .. '<map>   map id to fix, or "*" to fix all active maps',
                '\nNote that you can abbreviate both <type> and <map> parameters to a',
                'prefix, e.g. \"' .. _keyw('/td fix c *') .. '\" tries to fix corpses in all maps.'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(plr_name, params)
            local helpcmd = 'type "/td help fix" for help.'

            if not params then
                minetest.chat_send_player(plr_name, _info('Missing arguments, ' .. helpcmd))
            else
                local p1, p2, fix, map
                p1, p2, fix, map = params:find('(%w+)%s+([%w_*]+)')
                if not fix or not map then
                    minetest.chat_send_player(plr_name, _info('Missing arguments, ' .. helpcmd))
                else
                    if string.find('corpses', fix) then
                        tdc.fix_corpses(plr_name, map)
                    elseif string.find('map', fix) then
                        tdc.fix_map(plr_name, map)
                    else
                        minetest.chat_send_player(plr_name, _info('Unknown fix action, ' .. helpcmd))
                    end
                end
            end
        end,
    }
}

function tdc.exec(plr_name, params)
    local cmd = nil
    if params and params ~= '' then
        local par1 = params:match('[%w_]+')
        local parN = params:sub(par1:len() + 1) or ''
        local cname
        if tdc.actions[par1] then
            cname = par1
            cmd = tdc.actions[par1]
        else
            -- try cmd prefix match
            for cmdname in pairs(tdc.actions) do
                if cmdname:find(par1) == 1 then
                    cname = cmdname
                    cmd = tdc.actions[cmdname]
                end
            end
        end
        if cname then
            minetest.log('action', plr_name .. ' runs "/td ' .. cname .. parN .. '"')
            cmd.exec(plr_name, parN)
        else
            minetest.chat_send_player(plr_name,
                _info('Unknown command "' .. par1 .. '", type "/td help" for possible commands.'))
        end
    else
        minetest.chat_send_player(plr_name, list_actions())
    end
    return true
end

minetest.register_chatcommand('td', {
   privs = {traitor_dev = true},
   params = '<cmd> [<args>]',
   description = 'Traitor debugging commands.  Type "/td help" for a list of possible commands.',
   func = tdc.exec
})

